package model;

import java.util.BitSet;
import utils.BitUtils;
import utils.IllegalValueException;
import utils.UnsignedByte;

/**
 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
 * 
 */
public class IPAddress extends Address implements Comparable<IPAddress> {

	/**
	 * The IP protocol version (4 or 6)
	 */
	private int version;
	/**
	 * La subnet mask per questo indirizzo ip
	 */
	private String subnetMask;
	/**
	 * True se l'interfaccia e' appartenente alla rete di controllo
	 */
	private boolean isControlAddress;

	public IPAddress(int version, String address, String subnetMask)
			throws IllegalValueException {
		this(version, address, subnetMask, false);
	}

	/**
	 * @param version
	 * @param subnetMask
	 * @param isControlAddress
	 * @throws IllegalValueException
	 */
	public IPAddress(int version, String address, String subnetMask,
			boolean isControlAddress) throws IllegalValueException {
		super(address);
		// Cotralla se e' un IP valido
		toUnsignedBytes(address);

		this.version = version;
		this.subnetMask = subnetMask;
		// Controlla se e' nella forma valida
		toUnsignedBytes(subnetMask);
		this.isControlAddress = isControlAddress;
	}

	/**
	 * Costruttore di copia
	 * 
	 * @param address
	 */
	public IPAddress(IPAddress address) {
		super(address.getAddress());

		this.version = address.getVersion();
		this.subnetMask = address.getSubnetMask();
		this.isControlAddress = address.isControlAddress();
	}

	/**
	 * @return the version
	 */
	public int getVersion() {
		return version;
	}

	/**
	 * @param version
	 *            The IP protocol version (4 or 6) to set
	 */
	public void setVersion(int version) {
		this.version = version;
	}

	/**
	 * @return the subnetMask
	 */
	public String getSubnetMask() {
		return subnetMask;
	}

	/**
	 * @param subnetMask
	 *            the subnetMask to set
	 * @throws IllegalValueException
	 */
	public void setSubnetMask(String subnetMask) throws IllegalValueException {
		toUnsignedBytes(subnetMask);
		this.subnetMask = subnetMask;
	}

	/**
	 * 
	 * @return
	 */
	public boolean isControlAddress() {
		return isControlAddress;
	}

	/**
	 * 
	 * @param isControlInterface
	 */
	public void setControlAddress(boolean isControlInterface) {
		this.isControlAddress = isControlInterface;
	}

	/**
	 * Ottiene questo indirizzo in forma di {@link UnsignedByte}
	 * 
	 * @return
	 */
	public UnsignedByte[] getUnsignedBytes() {
		try {
			return toUnsignedBytes(getAddress());
		} catch (IllegalValueException e) {
			// Non dovrebbe potersi mai verificare, poiche' e' fatto il
			// conntrollo alla modifica dell'address memorizzato
			throw new RuntimeException(e);
		}
	}

	/**
	 * Converte una stringa rappresentante un indirizzo in dotted notation in
	 * una rappresentazione in {@link UnsignedByte}
	 * 
	 * @param address
	 * @return
	 * @throws IllegalValueException
	 */
	public static UnsignedByte[] toUnsignedBytes(String address)
			throws IllegalValueException {
		UnsignedByte[] addrBytes = new UnsignedByte[4];
		String[] addrString = address.split("\\.");

		if (addrString.length != 4)
			throw new IllegalValueException(address + " is not a legal IP address");

		for (int i = 0; i < addrString.length; i++) {
			addrBytes[i] = new UnsignedByte(addrString[i]);
		}

		return addrBytes;
	}

	/**
	 * Fa una comparazione "grezza" fra due indirizzi tramutandoli in array di
	 * byte unsigned. Valuta byte per byte i due indirizzi, fino a quando non
	 * trova che il byte in esame di un indirizzo e' diverso da un altro. A
	 * questo punto e' fatta una normale comparazione fra due numeri.
	 */
	@Override
	public int compareTo(IPAddress arg0) {
		UnsignedByte[] addrBytes1 = getUnsignedBytes();
		UnsignedByte[] addrBytes2 = arg0.getUnsignedBytes();

		for (int i = 0; i < addrBytes2.length; i++) {
			if (addrBytes1[i].getValue() > addrBytes2[i].getValue())
				return 1;
			if (addrBytes1[i].getValue() < addrBytes2[i].getValue())
				return -1;
		}

		return 0;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see neptune.manager.model.topology.Address#setAddress(java.lang.String)
	 */
	/**
	 * Questo metodo non dovrebbe essere adoperato, poiche' non viene fatto
	 * nessun controllo sull'indirizzo impostato.
	 * 
	 * @deprecated
	 */
	@Override
	public void setAddress(String address) {
		super.setAddress(address);
	}

	/**
	 * 
	 * @param address
	 * @deprecated
	 */
	public void setAddress(UnsignedByte[] address) {
		setAddress(toAddressString(address));
	}

	/**
	 * Converte un indirizzo fornito come array di {@link UnsignedByte} (di
	 * dimensione 4) nella sua rappresentazione in dotted notation.
	 * 
	 * @param address
	 * @return
	 */
	public static String toAddressString(UnsignedByte[] address) {
		String addressStr = address[0] + "." + address[1] + "." + address[2]
				+ "." + address[3];
		return addressStr;
	}

	/**
	 * Metodo di utilita' per ottenere l'indirizzo della subnet partendo da
	 * indirizzo IP e subnet Mask. L'indirizzo della subnet e' restituito nella
	 * forma di un array di dimensione 4. Ciascun elemento dell'array e' un
	 * {@link UnsignedByte}
	 * 
	 * @param address
	 * @param subnet
	 * @return
	 * @throws IllegalValueException
	 */
	public static UnsignedByte[] getSubnetAddress(UnsignedByte[] address,
			UnsignedByte[] subnet) throws IllegalValueException {

		UnsignedByte[] subnetAddr = new UnsignedByte[4];

		for (int i = 0; i < subnetAddr.length; i++) {
			BitSet addrByte = BitUtils.getBitValue(address[i]);
			BitSet subMByte = BitUtils.getBitValue(subnet[i]);

			addrByte.and(subMByte);

			subnetAddr[i] = BitUtils.getByteValue(addrByte);
		}

		return subnetAddr;
	}

	/**
	 * Restituisce il subnet address di questo indirizzo IP in
	 * {@link UnsignedByte}.
	 * 
	 * @return l'indirizzo IP in {@link UnsignedByte}
	 */
	public UnsignedByte[] getSubnetAddressInByte() {
		try {
			return getSubnetAddress(getUnsignedBytes(),
					toUnsignedBytes(subnetMask));
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Restituisce il subnet address di questo indirizzo IP come stringa.
	 * 
	 * @return
	 */
	public String getSubnetAddressString() {
		return toAddressString(getSubnetAddressInByte());
	}

	/**
	 * Restituisce il subnet address di questo indirizzo IP
	 * 
	 * @return
	 */
	public IPAddress getSubnetAddress() {
		try {
			return new IPAddress(version, getSubnetAddressString(), subnetMask);
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Verifica se l'indirizzo di subnet passato come primo argomento include
	 * l'indirizzo (di subnet o di host) passato come secondo argomento. In
	 * altre parole, verifica se newNet e' residente sulla subnet existingNet.
	 * 
	 * @param existingSubnetAddress
	 * @param newNet
	 * @return
	 * @throws IllegalValueException
	 */
	public static boolean isIncluded(IPAddress existingSubnetAddress,
			IPAddress newNet) throws IllegalValueException {

		// Ottiene il subnetAddress di newNet in UnsignedByte
		UnsignedByte[] newBytes = getSubnetAddress(newNet.getUnsignedBytes(),
				IPAddress.toUnsignedBytes(newNet.getSubnetMask()));

		// Fa l'end bit a bit fra la subnet di newNet e la subnetMask di
		// existingNet. Se newNet e' incluso in existingNet l'indirizzo
		// risultante sara' uguale a quello di existingNet
		UnsignedByte[] newSub = getSubnetAddress(newBytes, IPAddress
				.toUnsignedBytes(existingSubnetAddress.getSubnetMask()));

		String subStr = IPAddress.toAddressString(newSub);

		return existingSubnetAddress.getAddress().equals(subStr);
	}

	/**
	 * Verifica se l'indirizzo di subnet passato come primo argomento include
	 * l'indirizzo (di subnet o di host) passato come secondo argomento. In
	 * altre parole, verifica se newNet e' residente sulla subnet existingNet.
	 * 
	 * @param existingSubnetAddress
	 * @param newNet
	 * @return
	 * @throws IllegalValueException
	 */
	public static boolean isIncluded(IPAddress existingSubnetAddress,
			String newNet) throws IllegalValueException {

		// Ottiene il subnetAddress di newNet in UnsignedByte
		UnsignedByte[] newBytes = IPAddress.toUnsignedBytes(newNet);

		// Fa l'end bit a bit fra newNet e la subnetMask di
		// existingNet. Se newNet e' incluso in existingNet l'indirizzo
		// risultante sara' uguale a quello di existingNet
		UnsignedByte[] newSub = getSubnetAddress(newBytes, IPAddress
				.toUnsignedBytes(existingSubnetAddress.getSubnetMask()));

		String subStr = IPAddress.toAddressString(newSub);

		return existingSubnetAddress.getAddress().equals(subStr);
	}

	/**
	 * Nel caso in cui questo indirizzo rappresenti un indirizzo di subnet,
	 * verifica se l'indirizzo passato come argomento e' residente su questa
	 * subnet.<br>
	 * <br>
	 * Se questo indirizzo e' un'indirizzo di host, la chiamata ha lo stesso
	 * effetto di {@link IPAddress#equals(Object)} e non andrebbe usata in tal
	 * senso.
	 * 
	 * @param address
	 * @return
	 */
	public boolean include(IPAddress address) {
		try {
			return isIncluded(this, address);
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Nel caso in cui questo indirizzo rappresenti un indirizzo di subnet,
	 * verifica se l'indirizzo passato come argomento e' residente su questa
	 * subnet.<br>
	 * <br>
	 * Se questo indirizzo e' un'indirizzo di host, la chiamata ha lo stesso
	 * effetto di {@link IPAddress#equals(Object)} e non andrebbe usata in tal
	 * senso.
	 * 
	 * @param address
	 * @return
	 */
	public boolean include(String address) {
		try {
			return isIncluded(this, address);
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Verifica se l'indirizzo passato e' un indirizzo di subnet
	 * 
	 * @param address
	 * @return
	 */
	public static boolean isSubnetAddress(IPAddress address) {
		return address.getSubnetAddressString().equals(address.getAddress());
	}

	/**
	 * Verifica se questo indirizzo e' un indirizzo di subnet
	 * 
	 * @return
	 */
	public boolean isSubnetAddress() {
		return isSubnetAddress(this);
	}

	/**
	 * Converte un bit set di 32 bit in un indirizzo IP espresso in un array di
	 * {@link UnsignedByte} di lunghezza 4. I bit del BitSet devono essere
	 * organizzati dal meno significativo al piu' significativo. UN esempio di
	 * come sono ordinati i bit di un indirizzo da passare a questo metodo e' il
	 * seguente:<br>
	 * <br>
	 * Indirizzo: 10.0.1.1<br>
	 * Contenuto del BitSet per ottenere l'indirizzo:<br>
	 * Valore binario: 10000000 10000000 00000000 01010000<br>
	 * Valore decimale: 1 1 0 10<br>
	 * <br>
	 * L'output del metodo e' invece nella consueta forma con l'UnsignedByte di
	 * indice 0 corrispondente alla cifra piu' a sinistra della dotted notation.
	 * 
	 * @param bits
	 * @return
	 * @throws IllegalValueException
	 */
	public static UnsignedByte[] getAddressBytesFromBits(BitSet bits)
			throws IllegalValueException {
		UnsignedByte[] bytes = new UnsignedByte[4];
		for (int i = 0; i < bytes.length; i++) {
			bytes[3 - i] = BitUtils.getByteValue(bits.get(i * 8, (i + 1) * 8));
		}
		return bytes;
	}
	
	
	public static void main(String[] args) throws IllegalValueException {
		
		IPAddress a = new IPAddress(4, "192.168.74.16","255.255.255.0");
		System.out.println(a.getAddress());
		
	} 
	
}
